/*
 * #%L
 * java model
 * %%
 * Copyright (C) 2009 - 2011 prefabware.com Stefan Isele
 * %%
 * All Rights Reserved.
 * This program is dual-licensed under the GNU General Public license
 * or a commercial license.
 * You may select to use one or the other of these licenses.
 * To obtain a commercial license please contact Stefan Isele at www.stefan-isele.de
 * Any modifications to this file must keep this entire header intact.
 * Redistribution, except as permitted by the selected license, is prohibited.
 * #L%
 */
package com.prefabware.jmodel.typefactory;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.prefabware.commons.JavaUtil;
import com.prefabware.commons.QualifiedName;
import com.prefabware.commons.reflection.ClassReflector.ClassVisitor;
import com.prefabware.jmodel.JArgument;
import com.prefabware.jmodel.JField;
import com.prefabware.jmodel.JMethod;
import com.prefabware.jmodel.JMethodDeclaration;
import com.prefabware.jmodel.JModifiers;
import com.prefabware.jmodel.JParameterizedType;
import com.prefabware.jmodel.JType;
import com.prefabware.jmodel.JTypeDeclaration;
import com.prefabware.jmodel.JTypeUtil;
import com.prefabware.jmodel.JTypeWithFields;
import com.prefabware.jmodel.JVisibility;
import com.prefabware.jmodel.code.JBlock;
import com.prefabware.jmodel.expression.JExpressionUtil;
import com.prefabware.jmodel.typefactory.TypeFactory.CacheKeyClass;

public abstract class JTypeClassVisitor extends ClassVisitor {
	private JType jType;
	private TypeFactory typeFactory;

	public JTypeClassVisitor(TypeFactory typeFactory, JType[] typeArguments) {
		super();
		this.typeFactory = typeFactory;
		//this.typeArguments = typeArguments;
	}

	protected List<JArgument> calcArguments(Type[] types, JMethodDeclaration dcl) {
		List<JArgument> list = new ArrayList<JArgument>();
		int argIndex = 0;
		for (Type parmType : types) {
			JParameterizedType parameterizedType = typeFactory
					.calcClass(parmType);
			//			JType pJType = typeFactory.getJType(pClass);
			//			List<JType> typeArguments = calcTypeArguments(pClass);
			JArgument arg = JTypeUtil.newParameter(false, "arg"
					+ argIndex, parameterizedType.getRawType(), dcl, parameterizedType
					.getTypeArguments().toArray(new JType[0]));
		
			list.add(arg);
			argIndex++;
		}
		return list;
	}

	public JType getJType() {
		return jType;
	}

	@Override
	public void visitField(Field field) {
		if (this.getJType() instanceof JTypeWithFields) {
			JTypeWithFields withFields = (JTypeWithFields) this.getJType();
			Class<?> clazz = field.getDeclaringClass();
			int mod = field.getModifiers();
			JModifiers modifiers = JModifiers.getModifier(mod);
			JField jField = newField(field, clazz, mod, modifiers);
			withFields.add(jField);
		}
	}

	protected JField newField(Field field, Class<?> clazz, int mod,
			JModifiers modifiers) {
		Type genericFieldType = field.getGenericType();
		JParameterizedType parameterizedType = typeFactory
				.calcClass(genericFieldType);
		JType fieldType;
		fieldType = typeFactory.getJType(field.getType());

		JField jField = JTypeUtil.newJField(field.getName(), fieldType,getJType(),
				modifiers, JVisibility.getVisibility(mod), parameterizedType
						.getTypeArguments().toArray(new JType[0]));
		return jField;
	}

	protected List<JType> toJTypes(List<Class<?>> classes) {
		List<JType> jTypes = new ArrayList<JType>();
		for (Class<?> clazz : classes) {
			JType jTypeArg = typeFactory.getJType(clazz);
			jTypes.add(jTypeArg);
		}
		return jTypes;
	}

	@Override
	public void visitMethod(Method method) {
		int mod = method.getModifiers();
		JModifiers modifiers = JModifiers.getModifier(mod);
		//the actual type arguments of the method declaration
		JParameterizedType calcClass = typeFactory.calcClass(method
				.getGenericReturnType());
		JMethod newJMethod = newJMethod(method, getJType(),
				method.getName(), calcClass.getRawType(),
				modifiers, JVisibility.getVisibility(mod), calcClass.getTypeArguments());
		List<JArgument> arguments = calcArguments(method
				.getGenericParameterTypes(),newJMethod.getDeclaration());
		newJMethod.getArguments().addAll(arguments);
		getJType().add(newJMethod);
	}

	public JMethod newJMethod(Method method, JType declaringType,
			String name, JType returnType, JModifiers modifiers,
			JVisibility visibility, List<JType> typeArguments) {

		JMethod jmethod = JTypeUtil.newJMethod(name, returnType, declaringType,
				modifiers, visibility, typeArguments.toArray(new JType[0]));
		//add empty default implementation : return; or return null;
		jmethod.add(new JBlock().add(JExpressionUtil.returnFor(returnType)));
		return jmethod;
	}

	protected TypeFactory getTypeFactory() {
		return typeFactory;
	}

	private JType createComponentType(Class<?> cls) {
		JType componentType;
		if (cls.isArray()) {
			// drill down arrays down to the real class
			componentType = typeFactory.getJType(cls.getComponentType());
		} else {
			componentType = null;
		}
		return componentType;
	}

	@Override
	public void visitClass(Class<?> clazz) {
		QualifiedName qualifiedName = new QualifiedName(JavaUtil.getName(clazz));
		JModifiers modifiers = JModifiers.getModifier(clazz.getModifiers());
		JVisibility visibility = JVisibility
				.getVisibility(clazz.getModifiers());
		JType componentType = createComponentType(clazz);
		JTypeDeclaration declaration = new JTypeDeclaration(qualifiedName,
				modifiers, visibility, componentType);
		//TODO move to factory
		CacheKeyClass key = new CacheKeyClass(clazz);
		jType = newJType(qualifiedName, declaration);
		typeFactory.cache(key, jType);
		List<JType> typeArguments = createTypeArguments(clazz);
		//the subclasses may use another, specialized instance of JTypeDeclaration
		//set the typeArguments into the actually used decleration
		jType.getDeclaration().setTypeArguments(typeArguments);
	}

	protected List<JType> createTypeArguments(Class<?> clazz) {
		Type genericSuperclass = clazz.getGenericSuperclass();
		List<JType> typeArguments;
		if (genericSuperclass != null && genericSuperclass != Object.class
				&& genericSuperclass != clazz) {
			JParameterizedType parameterizedType = typeFactory
					.calcClass(genericSuperclass);
			typeArguments = parameterizedType.getTypeArguments();
		} else {
			typeArguments = Collections.emptyList();
		}
		return typeArguments;
	}

	protected abstract JType newJType(QualifiedName qualifiedName,
			JTypeDeclaration declaration, JType... typeArgument);
}
